"""
utils.web

Allows to do synchronous and asynchronous web requests over HTTP using a
variety of ways.
"""
import urllib
import urllib2
import socket
import threading
import subprocess
from path import path
from collections import deque
from utils.decorators import cache
from utils import constants
from utils.delay import DelayManager, StopLoop
from utils.compat import debug, msg
from utils.debug import debug_wrap

#===============================================================================
# Globals
#===============================================================================

delay_manager = DelayManager('utils_web')

#===============================================================================
# Custom Exceptions
#===============================================================================

class MissingDependency(Exception):
    """
    Exception raised by backend which cannot work on this system
    """

#===============================================================================
# Request and Response Objects
#===============================================================================

class Request(object):
    """
    A request object is used to send requests. You may also resend requests.
    """
    def __init__(self, url, data, backend, async, callback):
        """
        @param url: The URL of this request.
        @param data: The POST data of this request.
        @param backend: The backend to use for this request.
        @param async: Whether this request is asynchronous or not.
        @param callback: What callback to call after this request.
        """
        self.url        = url
        self.data       = data or {}
        self.backend    = backend
        self.async      = async
        self.callback   = callback
        
    @cache(by_num=0, by_name='self')
    def get_post_as_qs(self):
        return urllib.urlencode(self.data)
        
    def resend(self):
        """
        Resend the request.
        """
        self.backend.handle(self)
        
        
class Response(object):
    """
    A Response object holding the data and the request used to get this 
    response.
    Any request (be it async or sync) will get a Response object.
    """
    def __init__(self, data, request):
        self.data       = data
        self.request    = request


#===============================================================================
# Backends
#===============================================================================

class BaseBackend(object):
    """
    Base Backend for all backends. This is basically used to expose the high
    level API (async_request, request).
    """
    @debug_wrap
    def handle(self, request):
        """
        In real backends this method handles actual requests.
        
        @param request: A Request object to handle.
        """
        raise NotImplementedError, "BaseBackend needs to be subclassed"
    
    @debug_wrap
    def async_request(self, url, callback, data=None):
        """
        Do a asynchronous request.
        
        @param url: The URL to request.
        @param callback: The callback to call with the response.
        @param data: The POST data to send to the server.
        """
        request = Request(url, data, self, True, callback)
        self.handle(request)
        
    @debug_wrap
    def request(self, url, data=None):
        """
        Do a synchronous request.
        
        @param url: The URL to request.
        @param data: The POST data to send to the server.
        """
        request = Request(url, data, self, False, None)
        return self.handle(request)
    
    def cleanup(self):
        pass


class CLIBaseBackend(BaseBackend):
    base_command        = ''
    base_options        = []
    async_base_options  = []
    sync_base_options   = []
    extra_options       = []
    disallowed_options  = []
    post_options        = []
    check_interval      = 2
    timeout             = 30
    tempfile            = constants.gamedir.joinpath('.utils.web.tmp')
    
    def __init__(self):
        self.queue = deque()
        self.loop  = None
        self.current_request = None
        self.current_timeleft = self.timeout
        self._delayer = delay_manager.get_reference(self)
    
    @debug_wrap
    def post_process(self, cmdstring, request):
        return cmdstring
    
    @debug_wrap
    def handle(self, request):
        """
        In real backends this method handles actual requests.
        
        @param request: A Request object to handle.
        """
        cmdstring = self.build_command(request)
        if request.async:
            self._handle_async(cmdstring, request)
        else:
            return self._handle_sync(cmdstring, request)
            
    @debug_wrap
    def _handle_async(self, cmdstring, request):
        """
        Handle the request asynchronously by either putting it into the queue or
        start the queue.
        """
        self.queue.append((cmdstring, request))
        if self.loop is None:
            self._handle_queue()
    
    @debug_wrap
    def _handle_sync(self, cmdstring, request):
        """
        Handle the request synchronously.
        """
        pipe = subprocess.Popen(cmdstring, shell=True,
                                stdout=subprocess.PIPE).stdout
        return Response(pipe.read(), request)
            
    def _handle_queue(self):
        """
        Handle the queue of asynchronous requests
        """
        if self.tempfile.isfile():
            self.tempfile.remove()
        cmdstring, request = self.queue.popleft()
        self.current_timeleft = self.timeout
        self.current_request = request
        subprocess.Popen(cmdstring, shell=True)
        if not self.loop:
            self.loop = self._delayer.loop(self.check_interval, self._check)
    
    def _check(self):
        """
        Check if the download happened and was a success.
        """
        if self.tempfile.isfile():
            response = Response(self.tempfile.bytes(), self.current_request)
            self.current_request.callback(response)
            if self.queue:
                self._handle_queue()
            else:
                raise StopLoop
        elif self.current_timeleft > 0:
            self.current_timeleft -= self.check_interval
        else:
            response = Response('timed out', self.current_request)
            self.current_request.callback(response)
            if self.queue:
                self._handle_queue()
            else:
                raise StopLoop

    @cache(by_num=1, by_name='request')
    def build_command(self, request):
        """
        Build the command string for a request
        """
        options = list(self.base_options)
        if request.async:
            options += self.async_base_options
        else:
            options += self.sync_base_options
        for option in self.extra_options:
            if option.startswith('-'):
                optname = option.split(' ')[0]
            elif option.startswith('--'):
                optname = option.split('=')[0]
            else:
                continue
            if not optname in self.disallowed_options:
                options.append(option)
        if request.data:
            options += self.post_options
        raw_cmdstring = '%s %s %s' % (self.base_command, ' '.join(options),
                                      request.url)
        return self.post_process(raw_cmdstring, request)
        
    def cleanup(self):
        if self.loop is not None:
            self.loop.cancel()
        if self.tempfile.isfile():
            self.tempfile.remove()
    

class WgetBackend(CLIBaseBackend):
    """
    Preferred backend using wget to handle requests.  
    """
    disallowed_options  = ['-O', '--output-document', '-q', '--quite',
                           '-b', '--background']
    base_options        = ['-q']
    async_base_options  = ['-O %(tempfile)s', '-b', '-o /dev/null']
    sync_base_options   = ['-O -']
    post_options        = ['--post-data=%(postqs)s']
    
    def __init__(self, wget_command='wget', wget_options=[],
                 tempfile=CLIBaseBackend.tempfile, check_interval=2,
                 timeout=CLIBaseBackend.timeout):
        """
        @param wget_command: Command to use for wget (allows to specify a
            different path, eg for a certain 'OS').
        @param wget_options: Extra options to pass to wget. -q, -b and -o are
            not allowed.
        @param tempfile: The placein the filesystem to store the temporarily
            downloaded file. This is only needed for async requests.
        @param check_interval: Every how many seconds the async request should
            poll if it's done or not. Lower number means better accuracy but
            less performance for your server.
        @param timeout: The default timeout for a request.
        """
        CLIBaseBackend.__init__(self)
        # Dependencies Check
        version = subprocess.Popen(wget_command + ' --version', shell=True,
                                   stdout=subprocess.PIPE).stdout.read()
        if not version.lower().startswith('gnu wget'):
            raise MissingDependency(
                "wget does not seem to be isntalled: %s" % version
            )
        self.base_command   = wget_command
        self.extra_options  = wget_options
        self.tempfile       = path(tempfile)
        self.check_interval = check_interval
        self.timeout        = timeout
        
    @debug_wrap
    def post_process(self, raw_cmdstring, request):
        return raw_cmdstring % {'tempfile': self.tempfile,
                                'postqs': request.get_post_as_qs()}
        
        
class CurlBackend(CLIBaseBackend):
    """
    Backend using curl, similar to the one using wget.
    """
    disallowed_options  = ['-o', '--output', '-s', '--silent']
    base_options        = ['-s']
    async_base_options  = ['-o %(tempfile)s']
    sync_base_options   = ['-o -']
    post_options        = ['-d %(postqs)s']
    
    def __init__(self, curl_command='curl', curl_options=[],
                 tempfile=CLIBaseBackend.tempfile, check_interval=2,
                 timeout=CLIBaseBackend.timeout):
        CLIBaseBackend.__init__(self)
        # Dependencies Check
        version = subprocess.Popen(curl_command + ' --version', shell=True,
                                   stdout=subprocess.PIPE).stdout.read()
        if not version.lower().startswith('curl'):
            raise MissingDependency(
                "curl does not seem to be isntalled: %s" % version
            )
        self.base_command   = curl_command
        self.extra_options  = curl_options
        self.tempfile       = path(tempfile)
        self.check_interval = check_interval
        self.timeout        = timeout
        
    @debug_wrap
    def post_process(self, raw_cmdstring, request):
        return raw_cmdstring % {'tempfile': self.tempfile,
                                'postqs': request.get_post_as_qs()}
        
        
class UrllibBackend(BaseBackend):
    """
    Backend using just python standard library (urllib & threading). This is the
    fallback backend.
    """
    class UrllibBackendThread(threading.Thread):
        """
        The Thread class for the UrllibBackend.
        """
        def __init__(self):
            self.queue = deque()
            self.running = False
            threading.Thread.__init__(self)
            
        def run(self):
            self.running = True
            while self.running:
                if not self.queue:
                    continue
                request = self.queue.popleft()
                try:
                    handler = urllib2.urlopen(request.url,
                                              request.get_post_as_qs())
                    data   = handler.read()
                except urllib2.URLError, e:
                    data = 'urllib2.URLError: %s' % e.message
                response = Response(data, request)
                request.callback(response)
    thread = UrllibBackendThread()
                
    def __init__(self, timeout=30):
        """
        @param timeout: Timeout for requests
        """
        socket.setdefaulttimeout(timeout)
    
    @debug_wrap
    def handle(self, request):
        """
        In real backends this method handles actual requests.
        
        @param request: A Request object to handle.
        """
        if request.async:
            self.thread.queue.append(request)
            if not self.thread.running:
                self.thread.start()
        else:
            try:
                handler = urllib.urlopen(request.url, request.get_post_as_qs())
                data = handler.read()
            except urllib2.URLError, e:
                data = 'urllib2.URLError: %s' % e.message
            return Response(data, request)
        
    def cleanup(self):
        self.thread.running = False